package cn.tedu.charging.order.service.impl;

import cn.tedu.charging.common.pojo.JsonResult;
import cn.tedu.charging.common.pojo.param.GunStatusUpdateParam;
import cn.tedu.charging.common.pojo.vo.StationInfoVO;
import cn.tedu.charging.common.pojo.vo.UserInfoVO;
import cn.tedu.charging.common.utils.JsonUtils;
import cn.tedu.charging.order.common.ChargingConstants;
import cn.tedu.charging.order.common.GunStatusConstants;
import cn.tedu.charging.order.common.MqttConstants;
import cn.tedu.charging.order.feign.DeviceClient;
import cn.tedu.charging.order.feign.UserClient;
import cn.tedu.charging.order.mqtt.MqttProducer;
import cn.tedu.charging.order.pojo.dto.ChargingDto;
import cn.tedu.charging.order.pojo.param.OrderAddParam;
import cn.tedu.charging.order.quartz.DeviceCheckJob;
import cn.tedu.charging.order.rabbitmq.RabbitMQProducer;
import cn.tedu.charging.order.service.OrderService;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    /**
     * 1. 生成订单号  (生成的逻辑)
     * 2. 修改充电枪的状态 为 使用中  (微服务架构 订单服务 调用 设备服务  服务之间调用/通信)
     * 3. 获取用户信息(车辆id)  (微服务架构  订单服务 调用 用户服务 服务之间调用)
     * 4. 获取场站的信息和设备信息  (微服务架构  订单服务 调用 设备服务 服务之间调用)
     * 5. 给设备发送开始充电的指令  (物联网 订单服务 调用 充电桩设备 服务设备之间调用)
     *
     * @param orderAddParam
     * @return
     */
    @Override
    public String createOrder(OrderAddParam orderAddParam) {
        log.debug("创建订单:{}", orderAddParam);
        //1 生成订单号
        log.debug("创建订单-生成订单号");
        String orderNo = getOrderNo();
        log.debug("创建订单-生成订单号:{}", orderNo);
        //2 更新枪的状态
        log.debug("创建订单-更新枪的状态:{}", orderAddParam.getGunId());
        Boolean success = updateGunStatus(orderAddParam.getGunId());
        log.debug("创建订单-更新枪的状态:{},结果:{}", orderAddParam.getGunId(), success);
        //3 获取用户信息
        log.debug("创建订单-获取用户信息:{}", orderAddParam.getUserId());
        UserInfoVO userInfoVO = getUserInfo(orderAddParam.getUserId());
        log.debug("创建订单-获取用户信息:{},结果:{}", orderAddParam.getUserId(), userInfoVO);
        //4 获取场站信息
        log.debug("创建订单-获取场站信息:{}", orderAddParam.getGunId());
        StationInfoVO stationInfoVO = getStationInfo(orderAddParam.getGunId());
        log.debug("创建订单-获取场站信息:{},结果:{}", orderAddParam.getGunId(), stationInfoVO);
        //5 todo
        startCharging(orderNo, orderAddParam.getPileId(), orderAddParam.getGunId());
        log.debug("创建订单-结果:{}", orderNo);
        //6. 创建设备检查任务，用来检查设备无响应
        log.debug("创建订单-创建设备检查任务-入参:{}",orderNo);
        try {
            DeviceCheckJob deviceCheckJob = new DeviceCheckJob(orderNo);
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
        log.debug("创建订单-创建设备检查任务成功-入参:{}",orderNo);
        log.debug("创建订单-给消息队列发送有生存时间的消息,实现延迟消息,用来处理超时订单 <<充电过程中的设备故障>>:{}",orderNo);
        Boolean result = sendMessage(orderNo);
        log.debug("创建订单-给消息队列发送有生存时间的消息,实现延迟消息,用来处理超时订单 <<充电过程中的设备故障>>:{},结果:{}",orderNo,result);
        return orderNo;
    }

    @Autowired
    private RabbitMQProducer rabbitMQProducer;

    /**
     * 给RabbitMQ发送消息 用来处理超时订单
     * @param orderNo
     */
    private Boolean sendMessage(String orderNo){
        //todo 如果想穿多个字段信息 可以封装为一个Dto
        rabbitMQProducer.send(orderNo);
        return true;
    }

    @Autowired
    private MqttProducer mqttProducer;

    /**
     * 订单服务 给 设备 发送开始充电的指令
     */
    private void startCharging(String orderNo, Integer pileId, Integer gunId) {
        // 1> Topic设计
        // 枪维度 场站id_枪id 场站id_桩id_枪id
        // 桩维度 场站id_桩id 消息里包含 枪信息
        // 从业务上讲 用枪维度 和 桩维度 都可以 ,
        // 用桩维度 的topic 会比 枪维度的 topic 数量更少,从而节省资源
        // topic = /topic/start/666
        String topic = MqttConstants.TOPIC_START_CHARGING_PREFIX + pileId;
        //2>消息的设计
        //类似与调用设备的接口,消息的设计其实就是接口入参的设计
        //思考 业务是什么,业务如果缺少什么入参,会导致业务无法开始,这些缺少入参就是核心入参
        //首先明确业务目标
        // 开始充电-->给指定的设备(设备信息) 发送特定的指令(开始充电指令) 给一个具体的用户的订单开始充电
        //参数包括 订单信息的唯一标识 订单号, 设备信息 枪id 告诉桩 用哪个枪 目标 给哪个用户的哪个订单开始充电
        ChargingDto chargingDto = new ChargingDto();
        chargingDto.setOrderNo(orderNo);
        chargingDto.setPileId(pileId);
        chargingDto.setGunId(gunId);
        chargingDto.setMsg(ChargingConstants.START_CHARGING);
        log.debug("订单服务发送开始充电指令到设备topic:{},消息:{}", topic, chargingDto);
        //1>自定义格式 每个入参用自定义的|分割 orderNo|pileId|gunId|msg
        //String orderNoStr = orderNo + "|" + pileId + "|" + gunId + "|" + ChargingConstants.START_CHARGING;
        //String 类 这些常用的方法要熟悉 split
        //String[] split = orderNoStr.split("|");
        //2>Json通用
        //如何把对象转换为Json?
        //消息的序列化 Spring-Boot 自动 做的序列化 对象转Json,Json转对象
        //Spring-Boot 是怎么做的序列化? 通过搜索发现 底层的实现是 Jackson
        //Jackson 是什么？java中的一款开源的json库 方便 对象转Json,Json转对象
        //https://cloud.tencent.com/developer/article/2242912 json库 Gson FastJson ...
        String message = JsonUtils.toJson(chargingDto);
        log.debug("订单服务发送开始充电指令到设备topic:{},消息:{}", topic, message);
        Boolean success = mqttProducer.send(topic, message);
        log.debug("订单服务发送开始充电指令到设备topic:{},消息:{},结果:{}", topic, message, success);
    }

    @Autowired
    private DeviceClient deviceClient;

    /**
     * 更新枪的状态
     */
    private Boolean updateGunStatus(Integer gunId) {
        GunStatusUpdateParam gunStatusUpdateParam = new GunStatusUpdateParam();
        gunStatusUpdateParam.setGunId(gunId);
        gunStatusUpdateParam.setStatus(GunStatusConstants.GUN_STATUS_BUSY); //todo 魔数 应该定义为常量或者枚举
        return deviceClient.updateGunStatus(gunStatusUpdateParam).getData();
    }

    /**
     * 通过枪id获取场站信息
     */
    private StationInfoVO getStationInfo(Integer gunId) {
        JsonResult<StationInfoVO> stationInfo = deviceClient.getStationInfo(gunId);
        if (stationInfo != null) {
            return stationInfo.getData();
        }
        return null;
    }

    @Autowired
    private UserClient userClient;

    /**
     * 订单服务通过 open-feign 服务之间通信工具 和 注册中心 和 负载均衡
     * 调用用户服务 获取用户车辆信息
     * @param userId
     * @return
     */
    private UserInfoVO getUserInfo(Integer userId) {
        JsonResult<UserInfoVO> userInfo = userClient.getUserInfo(userId);
        if (userInfo != null) {
            return userInfo.getData();
        }
        return null;
    }

    /**
     * 生成订单号
     * 唯一性
     * 1 自增的数据库主键  递增 不重复  分库分表不能保证唯一  会重复
     * 2 UUID 随机数 并且 唯一  不是递增的
     * <p>
     * 全局唯一性：不能出现重复的ID号，既然是唯一标识，这是最基本的要求。
     * 趋势递增：在MySQL InnoDB引擎中使用的是聚集索引，由于多数RDBMS使用B-tree的数据结构来存储索引数据，在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
     * 单调递增：保证下一个ID一定大于上一个ID，例如事务版本号、IM增量消息、排序等特殊需求。
     * 信息安全：如果ID是连续的，恶意用户的扒取工作就非常容易做了，直接按照顺序下载指定URL即可；如果是订单号就更危险了，竞对可以直接知道我们一天的单量。所以在一些应用场景下，会需要ID无规则、不规则。
     * <p>
     * 分布式id生成器
     * 雪花算法  https://www.jianshu.com/p/1af94260664a
     * 美团leaf https://tech.meituan.com/2017/04/21/mt-leaf.html
     * <p>
     * 现在业务刚刚开始 先自己生成
     * <p>
     * 100000 + 随机数 + 时间戳
     *
     * @return
     */
    private String getOrderNo() {
        // 1 前缀
        String start = "100000";
        Random random = new Random();
        // 2 生成10000以内的随机数
        int r = random.nextInt(10000);
        // 3 拼接时间戳
        long now = System.currentTimeMillis();
        return start + "_" + r + "_" + now;
    }
}